ThreeJS做3D地图

您所在的位置:网站首页 中国 立体 地图 ThreeJS做3D地图

ThreeJS做3D地图

2023-09-09 03:39| 来源: 网络整理| 查看: 265

简单使用 ThreeJS 参考文章:

郭隆帮技术博客

先来看一下一个简单的效果

3d-Map.gif

ThreeJS

官方就是这么简单的一个介绍

"Javascript 3D library"

OpenGL 是一个跨平台 3D/2D 的绘图标准,WebGL 则是 openGL 在浏览器上的一个实现。web 前端开发人员可以直接用 WebGL 接口进行编程,但 WebGL 只是非常基础的绘图 API,需要编程人员有很多的数学知识、绘图知识才能完成 3D 编程任务,而且代码量巨大。Threejs 对 WebGL 进行了封装,让前端开发人员在不需要掌握很多数学知识和绘图知识的情况下,也能够轻松进行 web 3D 开发,降低了门槛,同时大大提升了效率。

ThreeJS 的几个要素

(以下具体 API 可以参照文档)

1. 场景

场景就像环境一样,用来放我们要添加的所有东西。

this.scene = new THREE.Scene() 2. 相机

相机可以当作我们的眼睛,没有相机,我们就看不到任何东西。相机有很多种。

camera = new THREE.PerspectiveCamera(fov, aspect, near, far) camera.position.set(0, -18, 15) 3. 灯光

就像我们没有灯光就看不见东西一样,我们需要灯光来照亮东西。光分很多种,有环境光,点光源,聚光灯,平行光等等,不同的光的照亮效果不一样。

// 环境光 ambientLight = new THREE.AmbientLight(0xbbbbbb) scene.add(ambientLight) // 平行光 (与点光源不同 是从一个方向来 不是从一个点) directionalLight = new THREE.DirectionalLight(0x666666) directionalLight.position.set(10, -50, 300) scene.add(directionalLight) 4. 场景控制器

这里我们用它来控制场景里的所有东西,当我们拖动场景缩小放大的时候,实际上就是在控制相机移动。

controls = new OrbitControls(camera, renderer.domElement) 5. 渲染器

渲染器,把注册的所有东西渲染到场景中。

//注册渲染器 const canvas = document.querySelector('#map') renderer = new THREE.WebGLRenderer({ canvas, alpha: true }) //渲染 renderer.render(this.scene, this.camera) requestAnimationFrame(this.render) 绘制一个 3D 地图,并且带鼠标高亮

这里详细讲解一下如何画一个地图,并且做一个鼠标移动高亮的效果。

1.我们可以用 json 数据,收尾连线的方式,画出轮廓线和中国地图,再挤压画出的平面图形,让其成为有厚度的 3D 模型。

2.我们也可以直接引入成型的地图模型,渲染就完事了。不过对于这种简单的模型,建议使用 json 数据画出来,因为加载模型的话,模型大小是一个问题,网页打开时加载模型需要一些时间,影响体验,而且,你得有一个愿意配合你的小伙伴。

我们采用上面所说的第一种方式

简单讲解一下画模型的思路: 首先,模型由两点构成 几何体 和 网格,我们可以理解为,一个没穿衣服的光秃秃模型和它的衣服。所以显然易见,我们选择一个几何体(或者画出一个几何体),然后给他穿个衣服(给他捯饬捯饬,弄点颜色透、明度之类的属性,也可以贴图上去)

1. 画出轮廓线 /* group-组,将一个国家的轮廓线放在同一个 group 中(分组),这样可以在场景中进行整体控制 如果不分组,在复杂场景中过多的模型对象会很混乱,难以维护 */ var group = new THREE.Group() // 一个国家多个轮廓线条line的父对象 /* pointArr:行政区一个多边形轮廓边界坐标(2个元素为一组,分别表示一个顶点x、y值) 通过BufferGeometry构建一个几何体,传入顶点数据 通过Line模型渲染几何体,连点成线 LineLoop和Line功能一样,区别在于首尾顶点相连,轮廓闭合 */ // 创建一个Buffer类型几何体对象 var geometry = new THREE.BufferGeometry() // 类型数组创建顶点数据 var vertices = new Float32Array(pointArr) // 创建属性缓冲区对象 var attribue = new THREE.BufferAttribute(vertices, 3) // 3个为一组,表示一个顶点的xyz坐标 // 设置几何体attributes属性的位置属性 geometry.attributes.position = attribue // 材质对象 var material = new THREE.LineBasicMaterial({ color: 0x008bfb // 线条颜色 }) // var line = new THREE.Line(geometry, material);// 线条模型对象(轮廓不闭合) var line = new THREE.LineLoop(geometry, material) // 首尾顶点连线,轮廓闭合 group.add(line) 2. 画出地图形状,并且挤压出厚度 var shapeArr = [] // 轮廓形状Shape集合 pointsArrs.forEach((pointsArr) => { var vector2Arr = [] // 转化为Vector2构成的顶点数组 pointsArr[0].forEach((elem) => { vector2Arr.push(new THREE.Vector2(elem[0] - this.offsetX, elem[1] - this.offsetY)) }) var shape = new THREE.Shape(vector2Arr) shapeArr.push(shape) }) // MeshBasicMaterial:不受光照影响 // MeshLambertMaterial:几何体表面和光线角度不同,明暗不同 var material1 = new THREE.MeshPhongMaterial({ color: this.bgColor, specular: this.bgColor }) var material2 = new THREE.MeshBasicMaterial({ color: 0x008bfb }) // 拉伸造型 var geometry = new THREE.ExtrudeBufferGeometry( shapeArr, // 多个多边形二维轮廓 // 拉伸参数 { // depth:根据行政区尺寸范围设置,比如高度设置为尺寸范围的2%,过小感觉不到高度,过大太高了 depth: height, // 拉伸高度 bevelEnabled: false // 无倒角 } ) var mesh = new THREE.Mesh(geometry, [material1, material2]) // 网格模型对象 3. 渲染模型

为什么要做响应式处理?

1.我们先来了解一下 requestAnimationFrame

[1] requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率

[2] 在隐藏或不可见的元素中,requestAnimationFrame 将不会进行重绘或回流,这当然就意味着更少的 CPU、GPU 和内存使用量

[3] requestAnimationFrame 是由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了 CPU 开销

2.即使用了 requestAnimationFrame,我们还要注意的是:

相机的会在每次render()时计算更新投影矩阵,但是如果渲染区域没有变化的话,我们就没有必要一直计算更新,所以我们在每次执行渲染函数时,计算出渲染区域大小是否发生变化,如果没有,就不去更新它,以此节省性能

// 响应处理函数 resizeRendererToDisplaySize(renderer) { const canvas = renderer.domElement const width = canvas.clientWidth const height = canvas.clientHeight // width和height是一开始记录的在网页种渲染区域的大小 const needResize = canvas.width !== width || canvas.height !== height if (needResize) { // 重新设置渲染器的渲染区域 renderer.setSize(width, height, false) } return needResize } //响应式渲染 render(){ if (this.resizeRendererToDisplaySize(renderer)) { const canvas = renderer.domElement // 重新计算相机的属性 camera.aspect = canvas.clientWidth / canvas.clientHeight // 更新相机的投影矩阵 camera.updateProjectionMatrix() } controls.update() renderer.render(scene, camera) // 智能刷新 this.globalID = requestAnimationFrame(this.render) } 4. 鼠标移动 区域高亮

实现方法:从一个地方发射一条射线,调用方法返回交叉点的数组,对穿过的模型进行操作

// 鼠标移动事件 高亮 handleMousemove(event) { // event.preventDefault() // 注册一个射线 this.raycaster = new THREE.Raycaster() // 计算出位置坐标 let mouse = new THREE.Vector2(0, 0) const canvas = document.querySelector('#map') mouse.x = (event.offsetX / canvas.offsetWidth) * 2 - 1 mouse.y = -(event.offsetY / canvas.offsetHeight) * 2 + 1 // 用一个新的原点和方向向量来更新射线 this.raycaster.setFromCamera(mouse, camera) // 检查射线和物体之间的所有交叉点(默认不包含后代) 返回的是对象数组 let intersects = this.raycaster.intersectObjects(meshGroup.children) // 取第一个穿过的模型对象 this.previousObj.material[0].color = new THREE.Color(this.bgColor) // 操作属性,此处为改变颜色,就达到了高亮的效果 if (intersects[0] && intersects[0].object) { intersects[0].object.material[0].color = new THREE.Color(0xffaa00) this.previousObj = intersects[0].object } }, 补充

页面卸载的时候 一定要将 各种渲染器 卸载 否则再次显示组件的时候可能出现一些奇怪的 bug 比如多一个卡住的地图

省会柱子、柱子顶部label、光圈效果

简单来说:

1、找一个光圈的png图片作为纹理创建一个矩形平面,并随机设置初始size,然后在render函数中改变他的大小和透明度(借助requestAnimationFrame不停渲染) 2、找一个渐变的图片作为纹理创建柱状模型(注意设置柱体透明和顶部开口) 3、创建CSS2D 和js创建元素差不多,然后用labelRenderer渲染(注意创建坐标位置) 代码

(offsetX和offsetY是中国地图大致的中心坐标 之所以使用到相关坐标时要减去这两个值,是为了让地图的中心在坐标原点)

1.创建过程

this.cityData.forEach((item) => { var pos = [item.longitude, item.latitude] // 每个省份行政中心位置经纬度 if (pos && pos.length > 0) { // 添加圆圈 var geometry = new THREE.PlaneGeometry(1, 1) // 矩形平面 // TextureLoader创建一个纹理加载器对象,可以加载图片作为几何体纹理 var textureLoader = new THREE.TextureLoader() // 执行load方法,加载纹理贴图成功后,返回一个纹理对象Texture textureLoader.load(require('../../../assets/images/光圈贴图.png'), (texture) => { var material = new THREE.MeshLambertMaterial({ color: 0xffffff, // 设置颜色纹理贴图:Texture对象作为材质map属性的属性值 map: texture, // 设置颜色贴图属性值 transparent: true // 使用背景透明的png贴图,注意开启透明计算 }) // 材质对象Material let mesh = new THREE.Mesh(geometry, material) var size = Math.random() * 3 + 2 // 2~5之间随机,表示mesh.size缩放倍数 mesh.scale.set(size, size, size) // 设置mesh大小 mesh.position.set(pos[0] - this.offsetX, pos[1] - this.offsetY, 2.3) // 设置mesh位置 mesh._s = size // mesh自定义一个属性表征大小 cityPointGroup.add(mesh) }) // 添加圆柱 let geometry2 = new THREE.CylinderGeometry(0.15, 0.15, 5, 32, 1, true) geometry2.rotateX(Math.PI / 2) geometry2.translate(0, 0, 1) // geometry2.cityData = item new THREE.TextureLoader().load(require('../../../assets/images/渐变.png'), (texture) => { var material2 = new THREE.MeshBasicMaterial({ map: texture, transparent: true, blending: THREE.AdditiveBlending }) var mesh2 = new THREE.Mesh(geometry2, material2) mesh2.position.set(pos[0] - this.offsetX, pos[1] - this.offsetY, 2.5) // 设置mesh位置 mesh2.cityData = item // 保存对应的城市数据 barGroup.add(mesh2) }) // 创建css2d Label this.labelRenderer = new CSS2DRenderer() var div = document.createElement('div') div.innerHTML = item.city + ': ' + item.count + '个' div.style.display = 'block' div.style.width = '8vw' div.style.height = '4vh' div.style.lineHeight = '4vh' div.style.textAlign = 'center' div.style.color = '#fff' const img = require('@/assets/images/kuang.png') div.style.background = 'url(' + img + ') no-repeat ' div.style.backgroundSize = '100% 100%' div.style.fontSize = '0.6349vw' div.style.position = 'absolute' div.style.backgroundColor = 'rgba(25,25,25,0.5)' div.style.borderRadius = '0.463vh' var label = new CSS2DObject(div) // 设置mesh位置 label.position.set(pos[0] - this.offsetX, pos[1] - this.offsetY, 5.5) this.previousObj = label scene.add(label) this.labelRenderer.setSize(renderer.domElement.clientWidth, renderer.domElement.clientHeight) this.labelRenderer.domElement.style.position = 'absolute' // 避免renderer.domElement影响HTMl标签定位,设置top为0px this.labelRenderer.domElement.style.top = '0px' this.labelRenderer.domElement.style.left = '0px' // 设置.pointerEvents=none,以免模型标签HTML元素遮挡鼠标选择场景模型 this.labelRenderer.domElement.style.pointerEvents = 'none' document.querySelector('#box').appendChild(this.labelRenderer.domElement) })

2.render中加入这一段

if (cityPointGroup.children.length) { cityPointGroup.children.forEach(function (mesh) { mesh._s += 0.02 mesh.scale.set(mesh._s, mesh._s, mesh._s) if (mesh._s 2.6 && mesh._s


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3